/** 更新到支持poi-3.16 */
package com.auxgroup.smarthome.utils.excel;

import com.auxgroup.smarthome.utils.excel.constant.SheetVersion;
import com.auxgroup.smarthome.utils.excel.editor.CellEditor;
import com.auxgroup.smarthome.utils.excel.editor.ColumnEditor;
import com.auxgroup.smarthome.utils.excel.editor.RegionEditor;
import com.auxgroup.smarthome.utils.excel.editor.RowEditor;
import com.auxgroup.smarthome.utils.excel.editor.SheetEditor;
import com.auxgroup.smarthome.utils.excel.util.ExcelUtil;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellStyle;
import org.apache.poi.ss.usermodel.FillPatternType;
import org.apache.poi.ss.usermodel.Font;
import org.apache.poi.ss.usermodel.HorizontalAlignment;
import org.apache.poi.ss.usermodel.Name;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.ss.util.CellRangeAddress;
import org.apache.poi.xssf.streaming.SXSSFWorkbook;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.apache.poi.xssf.usermodel.XSSFWorkbookType;

import java.io.BufferedOutputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.logging.Logger;

/**
 *
 *
 * <h1>重构使得支持最新Office版本,Excel97支持旧版，Excel2007支持新版</h1>
 *
 * <p>提供了更直观易用的常用方法。主要支持以下的功能：
 *
 * <ul>
 *   <li>实现链式调用方式
 *   <li>支持xls格式以及xlsx，可以加载已存在的xls和xlsx文件作为模板
 *   <li>提供了行编辑器、列编辑器和区域编辑器，可以一次操作一行、一列或一个区域的值或样式
 *   <li>可设置的样式包括：边框大小、颜色；背景色；字体大小、颜色、粗体、斜体、删除线、斜体等；数据格式；单元格宽高；对齐方式……等
 *   <li>设置打印样式、设置密码、按行或按列分组
 *   <li>插入图片、批注、公式
 * </ul>
 *
 * 详情请参考Demo例子和注释
 *
 * <p>核心类，能获取所有编辑器的实例。
 *
 * <p>主要有五种编辑器：1、行编辑器；2、列编辑器；3、区域编辑器；4、表编辑器；5、单元格编辑器
 *
 * <p>上述五种编辑器只能通过该类的工厂方法获取，而不能自行创建。
 *
 * @author laiqiuhua
 * @since 2017年5月1日
 */
public class Excel {

  private ExcelContext ctx;
  // 记录日志
  private Logger logger = Logger.getLogger(this.getClass().getTypeName());

  private Excel() {}

  /**
   * 创建office,可以选97或者2007，详情请看下列枚举，实际支持2010
   *
   * @see SheetVersion
   * @param sheetVersion
   */
  public Excel(SheetVersion sheetVersion) {
    Workbook workbook = null;
    switch (sheetVersion) {
      case EXCEL97:
        workbook = createExcel97();
        break;
      case EXCEL2007:
        workbook = createExcel2007();
        break;
      case EXCEL2007_SXSSF:
        workbook = createExcel2007();
        break;
      default:
        workbook = createExcel2007Stream();
    }
    ctx = new ExcelContext(this, workbook);
    setDefaultExcelStyle();
  }

  /**
   * 创建office97版本
   *
   * @return
   */
  private Workbook createExcel97() {
    Workbook workbook = null;
    try {
      workbook = new HSSFWorkbook();
    } catch (Exception e) {
      logger.warning(e.getMessage());
    }
    return workbook;
  }

  /**
   * 创建office2007版本
   *
   * @return
   */
  private Workbook createExcel2007() {
    Workbook workbook = null;
    try {
      workbook = new XSSFWorkbook(XSSFWorkbookType.XLSX);
    } catch (Exception e) {
      logger.warning(e.getMessage());
    }
    return workbook;
  }

  /**
   * 创建office2007版本
   *
   * @return
   */
  private Workbook createExcel2007Stream() {
    Workbook workbook = null;
    try {
      workbook = new SXSSFWorkbook();
    } catch (Exception e) {
      logger.warning(e.getMessage());
    }
    return workbook;
  }

  public Excel(Workbook workbook) {
    ctx = new ExcelContext(this, workbook);
  }

  /** 常见excel时设置的默认样式 */
  private void setDefaultExcelStyle() {
    Workbook workbook = this.getWorkbook();
    CellStyle tempCellStyle; // 临时的样式
    Font tempFont; // 临时的字体

    DefaultExcelStyle defaultStyle = new DefaultExcelStyle();
    ctx.setDefaultStyle(defaultStyle);

    setWorkingSheet(0);
    tempCellStyle = workbook.createCellStyle();
    ctx.setTempCellStyle(tempCellStyle);
    tempFont = workbook.createFont();
    ctx.setTempFont(tempFont);
    // 设置默认样式
    Cell cell = ExcelUtil.getCell(ctx.getWorkingSheet(), 0, 0);
    CellStyle cellStyle = cell.getCellStyle();
    cellStyle.setFillForegroundColor(defaultStyle.getBackgroundColor());
    cellStyle.setFillPattern(FillPatternType.NO_FILL);
    cellStyle.setAlignment(HorizontalAlignment.GENERAL);
    cellStyle.setFillPattern(defaultStyle.getFillPatternType());
    cellStyle.setAlignment(defaultStyle.getAlign());
    cellStyle.setVerticalAlignment(defaultStyle.getVAlign());
    // 设置边框样式
    cellStyle.setBorderBottom(defaultStyle.getBorderStyle());
    cellStyle.setBorderLeft(defaultStyle.getBorderStyle());
    cellStyle.setBorderRight(defaultStyle.getBorderStyle());
    cellStyle.setBorderTop(defaultStyle.getBorderStyle());
    cellStyle.setBottomBorderColor(defaultStyle.getBorderColor());
    cellStyle.setTopBorderColor(defaultStyle.getBorderColor());
    cellStyle.setLeftBorderColor(defaultStyle.getBorderColor());
    cellStyle.setRightBorderColor(defaultStyle.getBorderColor());
    // 默认字体
    Font font = workbook.createFont();
    font.setFontHeightInPoints(defaultStyle.getFontSize());
    font.setFontName(defaultStyle.getFontName());
    font.setColor(defaultStyle.getFontColor());
  }

  /**
   * 保存Excel文件到磁盘
   *
   * @param excelPath 保存路径,不需要加后缀名，会自动识别
   * @return true保存成功，false失败
   * @throws FileNotFoundException
   */
  public boolean saveExcel(String excelPath) {
    BufferedOutputStream fileOut;
    try {
      if (this.getWorkbook() instanceof HSSFWorkbook) {
        excelPath += ".xls";
      } else if (this.getWorkbook() instanceof XSSFWorkbook) {
        excelPath += ".xlsx";
      }
      fileOut = new BufferedOutputStream(new FileOutputStream(excelPath));
      return saveExcel(fileOut);
    } catch (FileNotFoundException e) {
      e.printStackTrace();
    }
    return false;
  }

  /**
   * 保存Excel文件，该方法完成操作后会关闭输出流
   *
   * @param fileOut 输出流
   * @return true保存成功，false失败
   */
  private boolean saveExcel(OutputStream fileOut) {
    boolean result = false;
    try {
      ctx.getWorkbook().write(fileOut);
      result = true;
    } catch (Exception e) {
      e.printStackTrace();
    } finally {
      try {
        fileOut.flush();
        fileOut.close();
      } catch (Exception e) {
        result = false;
      }
    }
    return result;
  }

  /** 关闭资源 */
  public void close() {
    try {
      this.getWorkbook().close();
    } catch (IOException e) {
      e.printStackTrace();
    }
  }

  /**
   * 选择第n个工作簿为工作状态，如果不存在则新建一个
   *
   * @param index 工作簿序号，从0开始
   * @return 第n个工作表
   */
  public SheetEditor setWorkingSheet(int index) {
    if (index < 0) {
      index = 0;
    }
    ctx.setWorkingSheet(ExcelUtil.getSheet(ctx.getWorkbook(), index));
    return this.sheet(index);
  }

  /**
   * 操作一个单元格
   *
   * @param row 行，从0开始
   * @param col 列，从0开始
   * @return 单元格编辑器
   */
  public CellEditor cell(int row, int col) {
    CellEditor cellEditor = new CellEditor(row, col, ctx);
    return cellEditor;
  }

  /**
   * 操作一行的单元格
   *
   * <p><b>注意：</b>如果要使用该方法设置一行的样式，请确保需要设置样式的单元格都已写入值，包括空值。否则请使用region方法选取区域
   *
   * @param row 行，从0开始
   * @return 行编辑器
   */
  public RowEditor row(int row) {
    return new RowEditor(row, ctx);
  }

  /**
   * 操作指定行，从第startCol列开始的单元格
   *
   * <p><b>注意：</b>如果要使用该方法设置一行的样式，请确保需要设置样式的单元格都已写入值，包括空值。否则请使用region方法选取区域
   *
   * @param row 行，从0开始
   * @param startCol 只操作指定的列之后的单元格
   * @return 行编辑器
   */
  public RowEditor row(int row, int startCol) {
    return new RowEditor(row, startCol, ctx);
  }

  /**
   * 该方法始终返回最后一个空白行的编辑器，当需要循环插入n行时特别有用
   *
   * <p>
   *
   * <blockquote>
   *
   * <pre>
   * for(int i=0;i&lt;data.size();i++){
   * 		excel.row().value(data.get(i));
   * }</pre>
   *
   * </blockquote>
   *
   * <p><b>注意：</b>如果要使用该方法设置一行的样式，请确保需要设置样式的单元格都已写入值，包括空值。否则请使用region方法选取区域
   *
   * @return 行编辑器
   */
  public RowEditor row() {
    int rowNum = ExcelUtil.getLastRowNum(ctx.getWorkingSheet());
    if (!checkEmptyRow(rowNum)) {
      rowNum++;
    }
    return new RowEditor(rowNum, ctx);
  }

  /**
   * 检查指定的行是否空行
   *
   * @param rowNum
   * @return
   */
  private boolean checkEmptyRow(int rowNum) {
    Row row = ctx.getWorkingSheet().getRow(rowNum);
    int lastCell = row != null ? row.getLastCellNum() : 2;
    return (lastCell == 1 || lastCell == -1);
  }

  /**
   * 操作一列的单元格<br>
   * <b>注意：如果要使用该方法设置一列的样式，请确保需要设置样式的单元格都已写入值，包括空值。否则请使用region方法选取区域</b>
   *
   * @param col 列，从0开始
   * @return 列编辑器
   */
  public ColumnEditor column(int col) {
    ColumnEditor columnEditor = new ColumnEditor(col, ctx);
    return columnEditor;
  }

  /**
   * 操作指定列中，从第startRow行<br>
   * <b>注意：如果要使用该方法设置一列的样式，请确保需要设置样式的单元格都已写入值，包括空值。否则请使用region方法选取区域</b>
   *
   * @param col 列，从0开始
   * @param startRow 只操作指定的行之后的单元格
   * @return 列编辑器
   */
  public ColumnEditor column(int col, int startRow) {
    ColumnEditor columnEditor = new ColumnEditor(col, startRow, ctx);
    return columnEditor;
  }

  /**
   * 操作一个区域的单元格，如合并、插入图片，调整样式等
   *
   * @param beginRow 开始行，从0开始
   * @param beginCol 开始列，从0开始
   * @param endRow 结束行，从0开始
   * @param endCol 结束列，从0开始
   * @return 区域编辑器
   */
  public RegionEditor region(int beginRow, int beginCol, int endRow, int endCol) {
    RegionEditor regionEditor = new RegionEditor(beginRow, beginCol, endRow, endCol, ctx);
    return regionEditor;
  }

  /**
   * 操作一个区域的单元格，如合并、插入图片，调整样式等
   *
   * @param ref 区域表达式，例如：$C$1:$H$1
   * @return 区域编辑器
   */
  public RegionEditor region(String ref) {
    RegionEditor regionEditor = new RegionEditor(CellRangeAddress.valueOf(ref), ctx);
    return regionEditor;
  }

  /**
   * 操作一个工作表，如设置表名、页眉页脚、打印格式、加密等
   *
   * @param index，从0开始
   * @return
   */
  public SheetEditor sheet(int index) {
    if (index < 0) {
      index = 0;
    }
    SheetEditor sheetEditor = new SheetEditor(ExcelUtil.getSheet(ctx.getWorkbook(), index), ctx);
    return sheetEditor;
  }

  /**
   * 选择处于工作状态的工作表
   *
   * @return
   */
  public SheetEditor sheet() {
    return this.sheet(ctx.getWorkingSheetIndex());
  }

  /**
   * 获取POI的工作簿对象
   *
   * @return
   */
  public Workbook getWorkbook() {
    return ctx.getWorkbook();
  }

  /**
   * 获取处于工作状态的工作表的序号
   *
   * @return 工作表序号，从0开始
   */
  public int getWorkingSheetIndex() {
    return ctx.getWorkingSheetIndex();
  }

  /**
   * 建立一个别名，别名为单元格引用、常量或公式提供了一个更简洁明了的引用名称<br>
   * 例如：createName("Interest_Rate","'Loan Calculator'!$E$5");
   *
   * @param name 别名
   * @param formulaText 引用、常量或公式
   * @return 新建的别名对象
   */
  public Name createName(String name, String formulaText) {
    Name refersName = ctx.getWorkbook().createName();
    refersName.setNameName(name);
    refersName.setRefersToFormula(formulaText);
    return refersName;
  }
}
